home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Amiga Game-Power
/
Amiga Game-Power.iso
/
pd mix ii
/
access
/
hddriver
/
article
< prev
next >
Wrap
Text File
|
1994-05-20
|
29KB
|
629 lines
[
This was written for a local newsletter. Any people mentioned
are locals.
Alan Kent
22 Wallabah St
Mt Waverley 3149
Alan Kent (RMIT, Melbourne, AUSTRALIA)
UUCP: {seismo,hplabs,mcvax,ukc,nttlab}!munnari!goanna.oz!ajk
ARPA: munnari!goanna.oz!ajk@SEISMO.ARPA
ACSnet: ajk@goanna.oz
]
AN INTRODUCTION TO WRITING
YOUR OWN DEVICE DRIVERS
Ron Wail and co. are not the only ones who have been
working on a hard disk for the
amiga. I have also been working on one for some time now
in the little spare time I
have. As I am currently doing my masters
in computer science at RMIT, I
do not have as much time as I would like to spend on the machine.
If you wish to buy a hard disk you will have to buy one off Ron and
friends as I have no intension of building and selling them.
If you want to take on the challenge of building your own,
then read on!
Getting the additional hardware needed to add a hard disk
to work was trivial compared
to the problems I had trying to get the software to work (this
may be because my brother John designed the hardware for me).
I used a WD-1002-05 controller card as my brother had one lying
around at home. I recently rang Daneva Australia in Sandringham
and they said the boards now cost approximately $550 plus 20% tax
(single quantities). Perhaps there are cheaper boards around these days.
The controller card can in fact handle 3 hard disks and 4 five
and a quarter inch floppy drives so you can also add some standard
5 inch floppies too if you like.
Using a prebuilt controller card made the hardware much simpler -
in fact, only 7 additional IC's were needed
for decoding and timing. At present I have the IC's on a wire
wrap board which plugs into the expansion slot on the side of the
amiga. Someday I hope to add a bit more memory and a battery backup
clock. I will explain the hardware in more detail later.
In this article, I thought I would explain some of the
fundamentals of trying to write a device driver. It may not be
enough for you to write your own driver immediately,
but its a step in the right direction.
Much of the information here is relevant for
any device driver, not just a hard disk device driver.
All of the information in this
article came from the manuals (somewhere), mainly the two
volumes of the RKM (Rom Kernal Manual). Much of the hardware
information I used came from the expansion specs which I ordered
from Commordore in the states for US$40(?).
As I am a C programmer by nature and dislike having to write
assembly language code (although it is faster), I have
added an extra twist to this project of trying to implement my drivers in C.
I think I should point out at this stage that it is almost impossible
to add hard disks to V1.1 (I tried for weeks, but I just could not
get AmigaDOS to realise that the hard disk existed)
It is very easy however with V1.2.
THE EXEC AND AMIGADOS
Before we can really get into device drivers, some basics of
the multitasking executive (called the exec) must be known. The exec
is the low level program that has a number of responsibilities, the
main responsibility being to decide which task is to
run next on the amiga. A task is simply an occurance of a program
running. For example, if two CLI windows are open at the same time,
then there are two CLI tasks running.
Each task on the amiga is given its own memory space and stack space.
Details about the task are kept in a task structure such as where
its stack space is and the contents of the task's registers
when the task is not running.
The exec also provides mechanisisms for supporting communication
between tasks, suppoirt for libraries of functions and support for devices
as well as keeping track of what memory has been allocated.
AmigaDOS is not part of the exec. It is another level of software
which runs above the exec. AmigaDOS is basically a file management system
which uses the exec functions. Its job is to keep
track of where files reside on the disk and provide
routines to read from and write to files.
When AmigaDOS starts running a program, it actually starts a
task, but adds some more information to the end of the task
structure such where as to direct console input and output.
Such tasks are called processes. Another slight difference is
that a pointer to a process points to the first field in
AmigaDOS's extra information after the task structure. The first entry
happens to contain a pointer to a message port which AmigaDOS
uses to communicate with the process (more on message ports later).
Care must be taken to determine
if a pointer is a pointer to a process or a task. In C, a process pointer
can easily be converted to a task pointer using the following code.
task = (struct Task *) ( ((char *)process) - sizeof(struct Task) )
SIGNALS
For one task to communicate to another task, the exec
provides a number of functions. Each task keeps 32 signals.
These signals can be set by other tasks using the Signal() function.
The task receiving the signal can then use the Wait() function
to wait for a signal to arrive.
When the Wait() function is called, the exec will remove
the task from the list of running tasks
until the desired signal arrives. This means that
no CPU time is used by the waiting process allowing more CPU time
for other processes.
A task can actually wait for any one of many different signals
to arrive from many different tasks at the same time.
Signals thus provide a very simple
synchronisation method between tasks. One problem with
signals however is that if the same signal is sent twice
to a task before the
receiving task gets a chance to have a look, it appears
as if only one signal is received.
MESSAGES
Signals by themselves are not particularly useful. What is more
useful is to be able to send some information from
one task to another. Messages and Ports provide this
ability. A message is simply a block of memory which has
a special structure at the beginning of the block followed
by any other information that is to be sent.
In C, this can be achieved by defining a structure as follows:
struct my_message {
/* first the standard message information */
struct Message msg;
/* and now my details */
int number;
char character;
int other_information;
char *string;
};
The message structure definition can be found in the include
file <exec/ports.h>. The Message structure has to be set up
in a special way which is described in the RKM exec manual,
page 34. Signals are used to notify a task that a new message
has been received.
PORTS
Ok, now we have a message structure, but how do we get a
message from one task to another? And how does one task know
which of the 32 signals to send to the other task to notify it
that a message has been sent? This is where ports become useful.
A port is like putting a letter box outside your house. It provides
a well known place that messages can be received.
A port can be created using
the function CreatePort() and when created, can be
assigned a unique name. The function FindPort() can be used
to search the whole system for a port by its name. When a port is created,
it is also allocated a signal number which is used to tell
the receiving task that a message has arrived.
SENDING MESSAGES
To send a message from one task to another then involves using the
PutMsg() function. PutMsg() requires a pointer to the
port (as returned by FindPort) and a pointer to the message
to be sent. The actual sending of the message involves
letting the receiving message port know where the message is in memory
and then sending the receiving task a signal to let it know
that a message has arrived. In order to allow many messages
to be sent to a single port, messages are kept in a queue.
This is why some special information is needed at the
beginning of each message structure - list pointers must be
maintained.
The receiving task normally waits for messages to arrive
using the WaitPort() function. This basically involves doing
a Wait() for the signal bit assigned to that port. Once a
message is received (or if a message has already been
received and the program had not noticed it yet) WaitPort()
will exit. Messages can then be removed form the port using
the GetMsg() function. Note that due to the problem
mentioned before about signals and the possibility of
multiple signals appearing to be only a single signal, many
calls to GetMsg() may be required. It is also possible that
WaitPort() will return that a new message has arrived but it
had already been processed - its just that the signal bit
had not yet been cleared. The normal code structure for
receiving messages can be safely performed using the
following C code:
struct Message *msg;
while ( 1 ) { /* loop forever */
WaitPort ( message_port );
while ( ( msg = GetMsg ( message_port ) ) != NULL ) {
/* do something with the message! */
}
}
Until a message has been received and processed, the sending
task should not modify the contents of the message. The
PutMsg() call is basically granting the receiving task
permission to use that piece of memory.
Once the receiving task has finished
with the message however, it may be useful to let the sending task
know. This is in fact very important if information is to be
returned to the sending task in the message structure. What
is done then is for the sending task to also create a
message port. This message port however is not given a name
and so cannot be found using FindPort(). Instead, a pointer
to this port is kept in the actual message structure
(in the field mn_ReplyPort). The task that receives the message
then uses the ReplyMsg() function to effectively send the
message back to the original task. This means the the
sending task must do a WaitPort() on the reply port so that
it will wait until the message is sent back. This allows complete
synchronization between the two tasks. Note that if the
reply port is not put into the message structure, then
ReplyMsg() does nothing.
To close off a port so that no more messages can be
received, use DeletePort(). The functions CreatePort() and
DeletePort() are actually support functions written in C which
should be in the libraries that come with your C compiler. These
functions allocate/deallocate the necessary memory and
call (if necessary) the functions AddPort() and RemPort().
For more details on ports and some
example code using them, see chapter 3 in RKM: exec.
WHAT IS A DEVICE DRIVER?
Well, now lets get into what this article is really about!
A device driver is a piece of code which provides an
interface between the amiga operating system or user
programs and (usually) hardware on the system. There is one
device driver for controlling the floppy disks, another for
controlling the printer and so on. Drivers usually reside on
disk until such time as they are opened. When a device
is opened, the amiga searches through its list of known
devices in memory and if it is not found, it then searches
the devs directory on disk. This is why the first time you
use the printer, the disk will become active for a period of
time (to load the device driver) but when using the printer
later, there may not be any extra disk activity (as the
driver is already loaded). Note that the amiga can try to
reclaim memory from a device driver if it is not in use and
the amiga is low on free memory. Using C it is not easy to
get this auto-load feature to work. As a result, I decided
to ignore it by instead adding a RUN command in my
startup-sequence file so that the driver was permanently
loaded and could not be removed. This may not be very elligent,
but it works!
In order
for a program to make a request to a device driver (for
example the printer driver), an OpenDevice() call must be
made. This
call has 4 parameters: the name of the device you wish to
open, the unit number you want (some devices such as the
floppy disks can have several units - for efficency they all share
the same device driver code), an IO request block
and some flags which are interpreted by the
device driver. The IO request block is a message with some extra
information added (see struct IORequest in <exec/io.h>).
This extra information includes a pointer to the device, a
pointer to special memory allocated per unit, a command
field, an error field and a flags field. As many devices
perform much the same sort of commands (for example, writing
to disk is not that much different than writing to a
printer) a standard form of requests has been defined. The
structure and basic commands defined are also in
<exec/io.h>.
Once a device is open, requests can be made using the IO
request block passed to the OpenDevice() call using the
functions CheckIO(), DoIO(), SendIO() and WaitIO().
DoIO() performs an IO operation by sending the device driver
the IO request as a message and waiting for the driver to
complete (signaled by a ReplyMsg()). To be more precise, DoIO()
tries to call the BeginIO entry point in the device driver directly
(explained later) which will either
immediately process the command and return, or else send
the message to the port for processing when the driver is ready
for it. SendIO() sends the request to the device driver's port,
but does not wait for completion. This allows the
task to continue doing other things. WaitIO() can then be
used to wait for the command to complete. CheckIO()
provides a means of testing if the operation is complete
without waiting for it if it has not. When CheckIO() says
the operation is complete, WaitIO() must still be called to
clear the signal bit. The RKM: libraries and devices
contains many pieces of example code opening devices,
sending commands and closing devices. Note that it is very
important to close a device when finished with it as some of
the devices only allow one task to open them at a time.
WRITING A DEVICE DRIVER
So now that we know what a device driver is, how do we go
about writing one? In the RKM: libraries and devices
appendix F a sample device driver written in assembler is
shown. Looking at the code, I suspect it contains a few errors.
Some pieces of code just didn't make sense to me. After
looking at it for a while and (hopefully) understanding how
it works, I threw it away and started again from scratch.
The example in the manual is meant to be able to
automatically load from disk. As I mentioned before, I
did not worry about this as I wanted to get something
working as quickly as possible and automatically loading C
programs looked like it could be a bit of a problem
(actually, one of the manuals stated it was not possible).
When writing a device driver, there are two main sections of code.
The first section looks very much like a library - and in
fact shares many structures and functions with libraries.
This provides a nice consistant interface between the device
driver and other programs that wish to use it.
The second section is what actually performs all the
commands.
First, a set of routines must be written which allow a
device to be opened, closed and expunged (totally removed from
memory). These are exactly the same as for a library. On top
of these functions, two extra functions must be defined - BeginIO
and AbortIO. BeginIO is a call which tries to
immediately perform the IO operation without using the
message port. If the operation cannot be done immediately,
then the command must be queued by sending it to the message
port. This allows simple status commands to be performed very
quickly. The AbortIO command tries to abort an existing
command. One problem with writing a driver in C code is that
the exec passes parameters for these calls in registers.
This means that some pieces of assembler must be written
to push the registers onto the stack before calling the C code.
Note that as the operating system is calling functions written
in C directly, if the C compiler requires special values in
the registers then the code will not work. For example, I have
heard that the Aztec C compiler uses a register to point to the
global variables. If this is the case, then this register must
be set up (probably by the same code that pushes the registers
onto the stack) before the actual C function can be called.
Once these routines are written a device structure
(struct Device) must be created using the MakeLibrary()
call. MakeLibrary() accepts a pointer to an area of memory
that defines how the library node is to be initialized (see
the function InitStruct()). However it is very difficult to
set up the necessary memory area for InitStruct() in C and so I simply
initialize the device structure after calling MakeLibrary()
using normal C code.
Once this has been done, AddDevice() must be called to
add the new device to the system. Once AddDevice() has been
called, other programs may use all of the device function
calls previously mentioned (OpenDevice() etc).
The example device driver in the manual
creates a new process for each unit. Rather than trying to
make more problems for myself by trying to create new
processes from a C program, and as I only have a single unit
anyway, I did not create a new process per unit but rather
used the same process as creates and adds the device node
to receive and process commands.
So, before the device is actually added to the system, a
port must be created to recieve commands. The Open code will
put a pointer to this port into the IO request block which
was passed in the OpenDevice() call.
This is how DoIO() and SendIO() know where to send the message.
THE HARD DISK DEVICE DRIVER
The above discussion should be able to be used for any type of device
driver you wish to write. I have been considering trying to
write a printer spooler using device drivers too, although
surely someone has done a printer spooler for the amiga
before (I had better go search through all the PD disks I guess).
The hard disk driver must emulate the trackdisk device driver in
every way. Legal requests are defined in chapter 7 of
the RKM: libraries and devices and include such commands as
read data from disk, write data to disk, format a track,
switch the motor on and so on. The commands are
all fairly straight forward. The
hard disk driver is much simpler than the floppy disk driver
in many ways as the disk cannot
be removed!
One area I found difficult to process was the
sector labels. The trackdisk device actually allows an extra
16 bytes per sector to be written due to some sector header
information on the disk. The hard disk controller I have
does not allow this. As a result, I mark any commands that
try to use this information as an error. So far, I have not
found any code that actually uses this information so it
is not a problem.
The trackdisk device driver (in V1.1 anyway)
buffers a whole track of the disk in memory at a time.
This buffering of data in memory is referred to as caching.
V1.2 does better caching, but I dont know how (I dont have
all the necessary documentation for V1.2).
As a result, I tried to add some of my own more intelligent
caching. Unfortunately, my caching seems to work quite well
for about 5 minutes, but then it hangs the system. I will
have to recheck the code sometime. It is difficult to
determine however if the caching actually improves the speed of the
hard disk or whether it runs slower due to the extra code
that needs to be executed. The extra caching certainly uses
up more memory, so at this stage I have removed all caching.
Even without caching, its still faster than floppies.
My hard disk driver then consists of a number of sections.
First, when it is loaded it creates and adds the device node
to the system. It then waits for comands to arrive from a
message port. When a command arrives, a switch statement
decides what code to execute based on the command type.
The disk read and write type commands map the offset and
length fields in the command message onto head, track and
sector numbers which is then fed to the hard disk controller.
Other commands such as motor on/off and disk change count
can be easily immitated (the motor can never be switched off
and the disk change count is constant).
One area that can be confusing is that
the code that is needed for handling open and close device
calls made by programs making requests to the driver is
never executed by the device driver process. When the device
is added to the system, a set of pointers to these functions
is put in the device structure allowing other tasks to execute
code.
To install the driver involves adding a few lines to your
S:STARTUP-SEQUENCE file. First the command RUN L:HARDDISK
(where L:HARDDISK is my device driver program) starts up
the device driver. Next the command MOUNT DH0: notifies
AmigaDOS that the device can be used as a disk. For the
mount command to work, the file DEVS:MOUNTLIST must have
an entry for DH0: added. The file on my disk had an example entry
for DF1: so I just copied it and changed the fields (such
as number of cylinders, heads etc) for my hard disk.
The name of the device driver also had to be changed from
trackdisk.device to harddisk.device. I have also added
to my S:STARTUP-SEQUENCE file
some ASSIGN commands to change the C: directory to be on
the hard disk. These changes need
only be made once and then every time you boot up, you
have a very big disk drive!
One problem I did have however was to make sure that the
hard disk driver was actually loaded before I started using
it. This is because the RUN command exits before the program
to be run has even been loaded from disk.
After the RUN command I put in a delay of about 5 seconds
in the S:STARTUP-SEQUENCE file
so that the driver should have time to load completely.
HARDWARE
This is a brief description of the hardware I have used.
It does not auto-configure (oh dear, Commodoore will never
support me now) as I could not work out exactly how to
do it. I have a copy of the expansion specs from Commodoore
(I sent off to the states) but the auto-configure was a bit
beyond the effort I wanted to go to. I am not likely to
add anything else to my amiga anyway (famous last words).
The controller card I used has 8 registers which I have mapped into
memory space. Due to
the 68000's 16 bit words, it ended up using 16 bytes of memory,
the high bytes of the 16 bit words not being used.
The controller's registers can be
read from or written to and include cyclinder number registers,
status registers, command registers and so on. Sending commands
to the hard disk simply involes storing the relevant parameters
in the controllers registers and sending a command such as
read a sector, write a sector or format a track.
All this is done by simple memory read and write commands.
I ended up putting the controller card at the top of expansion
memory at $9ffff0 (actually I do not decode the address to that
precision, but its close). As long as I dont expand to 8 megs
of memory, it should not interfere with anything - even other
boards which do autoconfigure.
After sending the controller a command, the device driver must
wait for the command to finish. I tried to use the interrupt line
provided by the controller card, but at this stage I have not
successfully got the interrupt handlers on the amiga to work. The approach
I am currently using is to poll the status register of the controller.
Polling is where the program sits in a loop continuously checking
the status register until the command has finished. In order to
give other tasks a chance to run while the driver is polling, I
placed a call to Delay() inside the polling loop. The delay cannot
be too long or else the disk will become too slow, but it does give
other tasks a bit of a chance to run.
Looking at the side of the amiga, the expansion bus is numbered
as follows. Reading the hardware reference manual seems to say
the numbering is different to this, but this is what I used (and
it works). I would check the signal numbers before building this
circuit in case of incorrect pin numbers due to typing errors.
1 3 5 7 9 ...... 85
=================== the edge of the board
2 4 6 8 10 ..... 86
WARNING: I do not take any responsibility if the following circuit
contains any errors. All I can say is that it has not blown my
amiga up yet. USE AT OWN RISK!
All numbers on the left are amiga signals. All numbers on the right
are WD-1002-05 controller signals. Parts should be LS or better for speed.
All signals marked * are active when low (inverted). Wires that cross like
|
---------
|
are not joined. Joins are shown by +'s as in
|
----+----
|
7404
1|\ 2 1|
74 AS* -----| >o----|
|/ 2|
59 A23 -------------|
3|\ 4 3|
57 A22 -----| >o----|---\
|/ | |8
5|\ 6 4|7430|o-----+
58 A21 -----| >o----| | |
|/ 5|---/ |
56 A20 -------------| |
6| |
54 A19 -------------| |
11| |
52 A18 -------------| |
12| |
47 A17 -------------| |
| | Address Decoding
|
1| |
45 A16 -------------| |
2| |
43 A15 -------------| |
3| |
41 A14 -------------|---\ |
4| | |
39 A13 -------------|7430|o--+ |
5| | | |
38 A12 -------------|---/ | |
6| | |
36 A11 -------------| | | ^
11| | | |
34 A10 -------------| 4o 5o 6|
12| +------------+
32 A9 -------------| | E0*E1*E2 O7|o-7--$9FFFC0--+-------- CS* 23
| | O6|o-9--$9FFF80 |
3| O5|o-10-$9FFF40 |
30 A8 ------------------|A2 O4|o-11-$9FFF00 |
2| 74138 O3|o-12-$9FFEC0 |9
28 A7 ------------------|A1 O2|o-13-$9FFE80\---/
1| O1|o-14-$9FFE40 \ /7404
23 A6 ------------------|A0 O0|o-15-$9FFE00 o8
+------------+ |
|
^ ^ |
| | |
^ 4o 10o |
| +-------+ +-------+ |
| 2| P |5 12| P |9 |
+---|D Q|-------------|D Q|-- |
| 7474 | | 7474 | | Timing
16 C1 ------+---|> Q*|o--- +----|> Q*|o--------+
| 3| C |6 | 11| C |8 | |
| +-------+ | +-------+ | |
| 1o | 13o | |
| | | | | | 1
+--------------------+ | | +--|----\ 3
| | | |7438 |o--+
+---------------------+----------+-----|----/ |
2 (openC) |
|
18 XRDY* ------------------------------------------------------------+
13|\ 12 1
68 R/W* ------+------| >o-------|----\ 3
| |/ 2|7400 |o--------- WR* 25
| +--|----/
| 11|\ 10 |
70 LDS* -------------| >o----+--|----\ 6
| |/ 4|7400 |o--------- RD* 27
+-----------------|----/
5
4
6 /---|-----+
19 INT2* --------o|7438| +------------------- INTRQ 35
\---|-----+
(openC) 5 Connect this only if want interrupts
after every command is finished
21 A5 ------- (not used)
24 A4 ------- (not used)
26 A3 ------- RS2 21
27 A2 ------- RS1 19
29 A1 ------- RS0 17
53 RES* ----- MR* 39
75 PD0 ------ DAL0 1
77 PD1 ------ DAL1 3
79 PD2 ------ DAL2 5
81 PD3 ------ DAL3 7
83 PD4 ------ DAL4 9
86 PD5 ------ DAL5 11
84 PD6 ------ DAL6 13
82 PD7 ------ DAL7 15
by Alan Kent